Lurch core classes

Tutorial: Free and bound variables

Free and bound variables

This tutorial assumes you know how to construct LC instances; if not, see the tutorial on Constructing LCs. This tutorial covers what it means for a symbol to be free or bound inside an LC tree.

(Each piece of sample code below is written as if it were a script sitting in the root folder of this source code repository, and run from there with the command-line tools node. If you place your scripts in another folder, you will need to adjust the path in each import statement accordingly. If you have not yet set up a copy of this repository with the appropriate Node.js version installed, see our GitHub README, which explains how to do so.)

Free variables vs. free symbols

Most situations that deal with the concepts of free/bound with respect to binding expressions speak of free and bound variables. But in the world of LCs, we do not have variables as a specific category; we have only the general category of symbols that can be used as variables or constants. Thus here we will speak of free and bound symbols.

The concept of a bound variable has the usual meaning: Inside any Binding expression (head sym1 sym2 ... symN , body), the symbols sym1 through symN are bound. Outside of that expression (assuming there is no containing binding expression of the same symbol(s)), they are not bound, and are thus called free.

However, in Lurch, we separate the operator (head in the above example) from the binding itself, so the code written above, (head sym1 sym2 ... symN , body) is equivalent to the more explicit version (head (sym1 sym2 ... symN , body)). The outer expression is a unary application of operator in question (here called head) and the inner expression is a "binding" that is equivalent to body but with several variables now bound.



import { LogicConcept } from './core/src/index.js'

const universalExample = LogicConcept.fromPutdown( '(forall x , (P x))' )[0]
universalExample.descendantsSatisfying( d => d.isAtomic() ).map( d => {
    console.log( 'Is', d.toPutdown(), 'free?', d.isFree() )
} )
// Console output:
Is forall free? true
Is x free? false
Is P free? true
Is x free? false


You can also ask the question relatively: Is a symbol free in a specific ancestor?



const outer = LogicConcept.fromPutdown( '(exists x , (forall y , (> x y)))' )[0]
const inner = outer.child( 1 ).body() // = (forall y , (> x y))
const secondX = inner.child( 1 ).body().child( 1 ) // = (> x y)
console.log( 'Is x free in the inner binding?', secondX.isFree( inner ) )
console.log( 'Is x free in the outer binding?', secondX.isFree( outer ) )
// Console output:
Is x free in the inner binding? true
Is x free in the outer binding? false


The isFree() method works not only for symbols. You can ask whether a nonatomic expression is free, and it will be free if and only if all of the symbols free within it are free in the given context.



// Is the (> x y) free in the (forall y , (> x y)) from above?
console.log( inner.child( 1 ).body().isFree( inner ) )
// Console output:
false


Free occurrences

We can also ask whether a symbol occurs free anywhere in an expression. Note the difference between x.occursFree(y) and y.occursFree(x) and that the order may be different from what you expect.



import { LurchSymbol } from './core/src/index.js'

const example = LogicConcept.fromPutdown( '(and (> x 1) (forall x , (R x x)))' )[0]
console.log( 'Last x free?', example.child( 2 ).child( 1 ).body().child( 2 ).isFree() )
console.log( 'Any x free?', example.occursFree( new LurchSymbol( 'x' ) ) )
// Console output:
Last x free? false
Any x free? true


You can get all free occurrences by combining descendantsSatisfying() with isFree().

Variable capture and replacing free occurrences

The usual notion of variable capture exists when considering substitution into a binding, and you can check to see if it will happen using isFreeToReplace(). You can do all of the free replacements with replaceFree().



console.log( 'Before:', example.toPutdown() )
example.replaceFree(
    new LurchSymbol( 'x' ),
    LogicConcept.fromPutdown( '(- 3 x)' )[0] )
console.log( 'After:', example.toPutdown() )
// Console output:
Before: (and (> x 1) (forall x , (R x x)))
After: (and (> (- 3 x) 1) (forall x , (R x x)))